\chapter{系统实现}
\section{微服务组件的实现}
系统设计采用Spring Cloud Alibaba微服务架构技术。Spring Cloud Alibaba是分布式架构的一整套解决方案。Spring Cloud Alibaba提供了构建分布式应用中所有常用功能的服务组件，加速开发分布式系统中的常用模式。\par
\subsection{服务注册与发现}
Spring Cloud Alibaba将Nacos划分为服务器端和服务客户端，该项目的Nacos的架构如图4.1所示。\par
\begin{figure}[H]
  \centering
  \includegraphics[width=0.8\textwidth]{Img/img4.1}
  \caption{Nacos架构图}
\end{figure}
服务生产者在启动时将自身信息注册到Nacos服务端，Nacos记录其地址及状态。服务消费者在需要调用服务时，先向Nacos进行服务发现，获取可用的服务实例列表，然后通过远程调用与服务生产者通信。整个过程中，Nacos作为注册中心，起到连接服务提供方与调用方的桥梁作用，实现了服务的动态注册与发现，提升了系统的可扩展性和灵活性。\par
\subsection{API网关}
API网关负责在客户端与后端微服务间转发数据，客户端通过API网关对不同的微服务发送请求。本文采用Spring Cloud Gateway组件实现API网关。Spring Cloud Gateway执行流程如图4.2所示。首先客户端发起请求至Spring Cloud Gateway，Gateway Handler Mapping根据配置路由规则进行匹配转发。\par
\begin{figure}[H]
  \centering
  \includegraphics[width=0.8\textwidth]{Img/img4.2}
  \caption{Spring Cloud Gateway工作流程图}
\end{figure}
API网关作为Spring Cloud Gateway的Gateway在启动时需要从Nacos服务器端获取路由映射的配置信息。另外作为Nacos客户端，API网关也需要在服务注册中心进行注册，因此需要指定Nacos服务器端IP地址和端口。\par
\subsection{远程调用}
借助Nacos，一个服务可以找到其他服务，但要访问它们则需进行远程调用，图4.3展示了 OpenFeign的工作原理。\par
\begin{figure}[H]
  \centering
  \includegraphics[width=0.8\textwidth]{Img/img4.3}
  \caption{OpenFeign原理图}
\end{figure}
服务消费者通过OpenFeign发起调用请求，Ribbon作为客户端负载均衡器根据策略选择一个服务提供者实例。OpenFeign将请求组装后发送至目标服务提供者，服务处理完成后再将响应返回。整个过程中，OpenFeign简化了调用逻辑，Ribbon实现了负载均衡，提升了系统的可扩展性与高可用性。图中多个服务提供者实例表示服务的集群部署。\par
\subsection{熔断降级}
如果某个服务在一短时间内收到巨量请求，可能会造成该服务崩溃，可用Sentinel组件进行限流、降级和熔断的操作，Sentinel的原理如图4.4所示。\par
\begin{figure}[H]
  \centering
  \includegraphics[width=0.8\textwidth]{Img/img4.4}
  \caption{Sentinel原理图}
\end{figure}
系统接收到大量请求后，Sentinel根据事先配置的限流、降级等规则对请求进行判断与处理。被允许的请求继续访问服务器资源，而超出规则限制的请求则被丢弃或降级处理，从而防止系统过载，保障整体稳定性。这种机制适用于高并发场景下的流量控制与服务熔断。\par
\section{主要功能模块的实现}
\subsection{用户管理服务的实现}
用户管理服务包括注册、登录、修改个人信息等服务，需要对用户表tb\_user进行增、删、改、查。\par
\begin{enumerate}
  \item 用户注册\par
  当用户在登录页面点击“注册”按钮，输入用户名、密码等需要初始化的值，点击“注册”按钮后如果信息无误，提示注册成功，否则提示用户修改有误字段。如图4.5所示。\par
  \begin{figure}[H]
    \centering
    \includegraphics[width=0.8\textwidth]{Img/img_register}
    \caption{用户注册页面}
  \end{figure}
  一旦系统接收到请求，它将对数据进行校验，其中用户名的验证是为了确认用户名是否已存在，而邮箱和手机的格式验证则是为了确保输入的邮箱和手机号码格式正确。将验证之后的所有数据验证，生成一个用户ID，并把ID记录下来，在tb\_user表中插入一条记录，把用户ID和对应的数据保存在其中，返回结果提示用户注册成功，并跳转到登录页。如图4.6所示。\par
  \begin{figure}[H]
    \centering
    \includegraphics[width=0.8\textwidth]{Img/img_login}
    \caption{用户注册后跳转到登录页面}
  \end{figure}
  存储用户密码时，不是直接存储，而是将密码与一个随机字符串拼接，然后生成MD5哈希值，并将这个哈希值作为密码存入tb\_user表中。\par
  \item 用户登录\par
  当用户需要登录时，输入户名和密码后，点击“登录”按钮发送请求。当系统接到请求后，先在用户表tb\_user中寻找输入的用户名记录，再计算输入的密码与存储的随机字符串拼接后的MD5哈希值，与记录中的密码字段的值进行比对。为保护用户隐私，未找到相应记录或密码不正确都提示“用户不存在或密码错误”。如图4.7所示。\par
  \begin{figure}[H]
    \centering
    \includegraphics[width=0.8\textwidth]{Img/img_login_error}
    \caption{用户登录失败页面}
  \end{figure}
  如果用户登录成功，提示“登录成功”，并跳转到首页。如图4.8所示。\par
  \begin{figure}[H]
    \centering
    \includegraphics[width=0.8\textwidth]{Img/img_login_ok}
    \caption{用户登录成功页面}
  \end{figure}
  服务器在验证成功用户输入的用户名、密码后生成令牌返回给客户端，客户端收到后解析该字符串，发现里面包含用户ID，即对服务器进行一个记忆，将用户ID作为key，用户名、用户身份作为值，保存在Redis服务器中，保存期限为一周。将用户名返回后用户登录结束，客户端完成用户登录后，客户端每次进行操作都将该令牌连同该操作一起发送服务器，用于验证权限。\par
  \item 修改个人信息\par
  在用户登录后，他们可以更新自己的个人信息，包括昵称和个人简介等其他资料。\par
  用户修改个人简介和其他信息时，只需输入新的内容，提交后，系统会进行判断信息格式是否正常，如果正常就更新tb\_user表中的用户资料信息，并给出返回值:修改成功。如图4.9所示。\par
  \begin{figure}[H]
    \centering
    \includegraphics[width=0.8\textwidth]{Img/img_info_change}
    \caption{修改个人信息页面}
  \end{figure}
  \item 用户退出登录\par
  用户在登录状态时，可以点击右上角昵称的下拉框，选择“退出登录”按钮，点击后会弹出一个确认窗口，询问是否退出登录，如果点击确认，则会删除Redis中保存的用户ID和用户名的映射关系，并跳转到登录页面。如图4.10所示。\par
  \begin{figure}[H]
    \centering
    \includegraphics[width=0.8\textwidth]{Img/img5.2}
    \caption{用户退出登录}
  \end{figure}
\end{enumerate}

\subsection{题库管理服务的实现}
题库管理服务具备的功能包括添加、删除和编辑题目等，其中添加和编辑题目的权限仅限于教师账号使用。题库管理服务需要涉及试题目表tb\_question。\par
\begin{enumerate}
  \item 题目查看\par
  所有用户（包括游客）都可查看题库中的题目，题库页面展示了所有题目的基本信息，包括题目ID、题目名称、难度、通过率等信息，如图4.11所示。
  \begin{figure}[H]
    \centering
    \includegraphics[width=0.8\textwidth]{Img/img_home}
    \caption{题库页面}
  \end{figure}
  用户可以点击题目右侧的“做题”按钮查看该题目的详细信息，如图4.12所示。\par
  \begin{figure}[H]
    \centering
    \includegraphics[width=0.8\textwidth]{Img/img_question}
    \caption{题目页面}
  \end{figure}
  \item 题目上传\par
  只有教师用户才能进行题目上传。教师可以点击页面顶部的“创建题目”按钮跳转到题目上传页面，要提交的表单信息包含题目名、题目时间空间限制、题目描述、测试用例等信息。待填写完测试数据之后，点击“提交”上传题目，如果上传成功，提示上传成功并将题目添加至题目表中。如图4.13所示。\par
  \begin{figure}[H]
    \centering
    \includegraphics[width=0.8\textwidth]{Img/img4.17}
    \caption{题目上传页面}
  \end{figure}
  \item 题目删除\par
  在登录后，教师能够在题库页面执行删除题目的操作，可以查看所有已上传的题目，并有权删除其中的任何一个。在管理题库页面，找到想删除的题目点击其右侧的“删除”按钮，确认删除后即可删除该题目，如图4.14所示。\par
  \begin{figure}[H]
    \centering
    \includegraphics[width=0.8\textwidth]{Img/img_delete}
    \caption{题目删除页面}
  \end{figure}
  当单击删除按钮后，会出现一个弹出窗口询问是否确定删除该题目，若点击确认后，会删除该题目。处理方法是，先查询当前登录用户是否具有题目删除权限，然后根据待删除题目ID，从tb\_question表中删除对应题目记录。\par
  \item 题目修改\par
  教师可以修改题目。在管理题库页面，点击要修改题目的右侧“修改”按钮，即可进入题目修改页面，教师用户可根据需要修改题目。如图4.15所示。\par
  \begin{figure}[H]
    \centering
    \includegraphics[width=0.8\textwidth]{Img/img_update}
    \caption{题目修改页面}
  \end{figure}
  系统会对用户权限进行校验，而后根据编辑请求中的题目ID和要编辑的内容在题目表tb\_question中进行更新。\par
\end{enumerate}

\subsection{代码管理服务的实现}
代码管理服务负责管理用户的提交记录、保存每次提交的源代码。用户可在导航栏点击“提交列表”按钮跳转到提交列表，在此页面可以看到所有人的提交记录，点击提交记录右侧的“查看代码”按钮可查看本人提交的代码，教师可以查看所有人的代码。\par
\begin{enumerate}
  \item 提交代码\par
  提交代码页面如图4.16所示。\par
  \begin{figure}[H]
    \centering
    \includegraphics[width=0.8\textwidth]{Img/img_question}
    \caption{提交代码页面}
  \end{figure}
  用户在题目详情页面点击“提交代码”按钮，跳转到提交代码页面，用户可以在此页面选择编程语言、输入代码。点击“提交”按钮后，系统会对用户的代码进行编译和运行。\par
  \item 提交列表\par
  查看历史提交的页面如图4.17所示。\par
  \begin{figure}[H]
    \centering
    \includegraphics[width=0.8\textwidth]{Img/img_history}
    \caption{历史提交页面}
  \end{figure}
  在此页面可以看到所有用户的提交记录，包括提交者、提交题目、提交代码的长度、代码语言，以及运行占用的系统资源和运行结果。\par
  \item 查看代码\par
  查看代码页面如图4.18所示。\par
  \begin{figure}[H]
    \centering
    \includegraphics[width=0.8\textwidth]{Img/img_code}
    \caption{查看代码页面}
  \end{figure}
  用户在提交列表页面点击“查看代码”按钮，跳转到查看代码页面，用户可以在此页面查看自己提交的代码，教师可以查看所有人的代码。\par
\end{enumerate}


\subsection{代码评测服务的实现}
\begin{enumerate}
  \item 评测模块\par
  评测模块在进行代码评测的过程中进行代码的编译、运行、验证结果。评测模块工作流程如图4.19所示。\par
  \newpage
  \begin{figure}[H]
    \centering
    \includegraphics[width=0.8\textwidth]{Img/img4.6}
    \caption{评测模块工作流程图}
  \end{figure}
  在评测模块的操作流程中，最关键的两个步骤是源码编译和程序运行，其中源码编译的流程如图4.20所示。\par
  父进程通过waitpid方法等待子进程的退出，实际是在等编译过程结束，当子进程结束时，父进程可得到子进程的一个退出码。退出码可以帮助判断编译结果是否成功：如果退出码为0，则说明编译成功；否则表示编译失败。\par
  \begin{figure}[H]
    \centering
    \includegraphics[width=0.8\textwidth]{Img/img4.7}
    \caption{编译源码执行流程图}
  \end{figure}
  当代码编译成功后，会生成可执行程序，此时对生成的文件进行程序执行，然后对指定数据进行输入，获取输出数据并跟标准答案进行对比，然后得出鉴定结果。执行的详细过程如图4.21所示。\par
  \begin{figure}[H]
    \centering
    \includegraphics[width=0.8\textwidth]{Img/img4.8}
    \caption{运行程序执行流程图}
  \end{figure}
  \item 评测安全\par
  为了保证系统安全运行，需要重视以下几个方面的问题：(1)恶意系统调用，用户可能会在上传的代码中进行危险的系统调用，进而破坏服务器；(2)资源管理，用户可能会在上传的代码中消耗系统资源，如果包含死循环、申请巨量内存、创建巨量子进程等）。\par
  系统调用的安全性方面，在线评测模块基于Linux操作系统。评测服务器中给用户分配的Docker镜像只允许必要的系统调用，并且Docker容器和真实机互相隔离，系统调用的安全性得以有效保护。\par
  防止资源消耗方面，只为Docker容器分配有限的内存资源，限制了运行时间。当一个容器运行超过5秒就直接kill此容器。Docker与生俱来的隔离性很好地防止了恶意代码的资源消耗。\par
  下面是用户提交代码后能看到的几种评测结果页面。\par
  图4.22展示了没有提交正在排队时的评测页面。此时RunId为133号的提交正在评测。\par
  \begin{figure}[H]
    \centering
    \includegraphics[width=0.8\textwidth]{Img/img4.27}
    \caption{评测结果页面1}
  \end{figure}
  图4.23展示了有提交正在排队时的评测页面。此时133号提交正在测试，134号提交正在队列中处于就绪状态，等待分配到资源进行评测。\par
  \begin{figure}[H]
    \centering
    \includegraphics[width=0.8\textwidth]{Img/img4.28}
    \caption{评测结果页面2}
  \end{figure}
  图4.24展示了RunId为134号的提交评测完成的页面。134号提交在评测时触发了超时异常，此时135号提交正在评测。\par
  \begin{figure}[H]
    \centering
    \includegraphics[width=0.8\textwidth]{Img/img4.30}
    \caption{评测结果页面3}
  \end{figure}
  代码评测服务对外开放的接口如表4.1所示，采用HTTP的POST请求，接收URL为"/judge"，请求体中包含JudgeArgs格式的JSON字符串，返回类型为JudgeResult。\par
  \begin{table}[!htbp]
    \caption{代码评测服务接口}
    \centering
    \setlength{\tabcolsep}{5pt}% column separation
    \renewcommand{\arraystretch}{1.5}%row space 
    \begin{tabular*}{35cc}{@{\extracolsep{\fill}}cccc}
      \toprule[2pt]
      请求方法 & URL & 请求体 & 说明\\
      \midrule
      POST & /judge & JudgeArgs的JSON字符串 & 评测代码，返回JudgeResult\\
      \bottomrule[2pt]
    \end{tabular*}
  \end{table}
\end{enumerate}
